//
//  ========================================================================
//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package com.baffalotech.integration.http.netty.core.util;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/* ------------------------------------------------------------ */
/**
 * TYPE Utilities. Provides various static utility methods for manipulating
 * types and their string representations.
 *
 * @since Jetty 4.1
 */
public class TypeUtil {
	private static final Logger LOG = LoggerFactory.getLogger(TypeUtil.class);
	public static final Class<?>[] NO_ARGS = new Class[] {};
	public static final int CR = '\015';
	public static final int LF = '\012';

	/* ------------------------------------------------------------ */
	private static final HashMap<String, Class<?>> name2Class = new HashMap<>();
	static {
		name2Class.put("boolean", java.lang.Boolean.TYPE);
		name2Class.put("byte", java.lang.Byte.TYPE);
		name2Class.put("char", java.lang.Character.TYPE);
		name2Class.put("double", java.lang.Double.TYPE);
		name2Class.put("float", java.lang.Float.TYPE);
		name2Class.put("int", java.lang.Integer.TYPE);
		name2Class.put("long", java.lang.Long.TYPE);
		name2Class.put("short", java.lang.Short.TYPE);
		name2Class.put("void", java.lang.Void.TYPE);

		name2Class.put("java.lang.Boolean.TYPE", java.lang.Boolean.TYPE);
		name2Class.put("java.lang.Byte.TYPE", java.lang.Byte.TYPE);
		name2Class.put("java.lang.Character.TYPE", java.lang.Character.TYPE);
		name2Class.put("java.lang.Double.TYPE", java.lang.Double.TYPE);
		name2Class.put("java.lang.Float.TYPE", java.lang.Float.TYPE);
		name2Class.put("java.lang.Integer.TYPE", java.lang.Integer.TYPE);
		name2Class.put("java.lang.Long.TYPE", java.lang.Long.TYPE);
		name2Class.put("java.lang.Short.TYPE", java.lang.Short.TYPE);
		name2Class.put("java.lang.Void.TYPE", java.lang.Void.TYPE);

		name2Class.put("java.lang.Boolean", java.lang.Boolean.class);
		name2Class.put("java.lang.Byte", java.lang.Byte.class);
		name2Class.put("java.lang.Character", java.lang.Character.class);
		name2Class.put("java.lang.Double", java.lang.Double.class);
		name2Class.put("java.lang.Float", java.lang.Float.class);
		name2Class.put("java.lang.Integer", java.lang.Integer.class);
		name2Class.put("java.lang.Long", java.lang.Long.class);
		name2Class.put("java.lang.Short", java.lang.Short.class);

		name2Class.put("Boolean", java.lang.Boolean.class);
		name2Class.put("Byte", java.lang.Byte.class);
		name2Class.put("Character", java.lang.Character.class);
		name2Class.put("Double", java.lang.Double.class);
		name2Class.put("Float", java.lang.Float.class);
		name2Class.put("Integer", java.lang.Integer.class);
		name2Class.put("Long", java.lang.Long.class);
		name2Class.put("Short", java.lang.Short.class);

		name2Class.put(null, java.lang.Void.TYPE);
		name2Class.put("string", java.lang.String.class);
		name2Class.put("String", java.lang.String.class);
		name2Class.put("java.lang.String", java.lang.String.class);
	}

	/* ------------------------------------------------------------ */
	private static final HashMap<Class<?>, String> class2Name = new HashMap<>();
	static {
		class2Name.put(java.lang.Boolean.TYPE, "boolean");
		class2Name.put(java.lang.Byte.TYPE, "byte");
		class2Name.put(java.lang.Character.TYPE, "char");
		class2Name.put(java.lang.Double.TYPE, "double");
		class2Name.put(java.lang.Float.TYPE, "float");
		class2Name.put(java.lang.Integer.TYPE, "int");
		class2Name.put(java.lang.Long.TYPE, "long");
		class2Name.put(java.lang.Short.TYPE, "short");
		class2Name.put(java.lang.Void.TYPE, "void");

		class2Name.put(java.lang.Boolean.class, "java.lang.Boolean");
		class2Name.put(java.lang.Byte.class, "java.lang.Byte");
		class2Name.put(java.lang.Character.class, "java.lang.Character");
		class2Name.put(java.lang.Double.class, "java.lang.Double");
		class2Name.put(java.lang.Float.class, "java.lang.Float");
		class2Name.put(java.lang.Integer.class, "java.lang.Integer");
		class2Name.put(java.lang.Long.class, "java.lang.Long");
		class2Name.put(java.lang.Short.class, "java.lang.Short");

		class2Name.put(null, "void");
		class2Name.put(java.lang.String.class, "java.lang.String");
	}

	/* ------------------------------------------------------------ */
	private static final HashMap<Class<?>, Method> class2Value = new HashMap<>();
	static {
		try {
			Class<?>[] s = { java.lang.String.class };

			class2Value.put(java.lang.Boolean.TYPE, java.lang.Boolean.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Byte.TYPE, java.lang.Byte.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Double.TYPE, java.lang.Double.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Float.TYPE, java.lang.Float.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Integer.TYPE, java.lang.Integer.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Long.TYPE, java.lang.Long.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Short.TYPE, java.lang.Short.class.getMethod("valueOf", s));

			class2Value.put(java.lang.Boolean.class, java.lang.Boolean.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Byte.class, java.lang.Byte.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Double.class, java.lang.Double.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Float.class, java.lang.Float.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Integer.class, java.lang.Integer.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Long.class, java.lang.Long.class.getMethod("valueOf", s));
			class2Value.put(java.lang.Short.class, java.lang.Short.class.getMethod("valueOf", s));
		} catch (Exception e) {
			throw new Error(e);
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * Array to List.
	 * <p>
	 * Works like {@link Arrays#asList(Object...)}, but handles null arrays.
	 * 
	 * @param a the array to convert to a list
	 * @return a list backed by the array.
	 * @param <T> the array and list entry type
	 */
	public static <T> List<T> asList(T[] a) {
		if (a == null)
			return Collections.emptyList();
		return Arrays.asList(a);
	}

	/* ------------------------------------------------------------ */
	/**
	 * Class from a canonical name for a type.
	 * 
	 * @param name A class or type name.
	 * @return A class , which may be a primitive TYPE field..
	 */
	public static Class<?> fromName(String name) {
		return name2Class.get(name);
	}

	/* ------------------------------------------------------------ */
	/**
	 * Canonical name for a type.
	 * 
	 * @param type A class , which may be a primitive TYPE field.
	 * @return Canonical name.
	 */
	public static String toName(Class<?> type) {
		return class2Name.get(type);
	}

	/* ------------------------------------------------------------ */
	/**
	 * Convert String value to instance.
	 * 
	 * @param type  The class of the instance, which may be a primitive TYPE field.
	 * @param value The value as a string.
	 * @return The value as an Object.
	 */
	public static Object valueOf(Class<?> type, String value) {
		try {
			if (type.equals(java.lang.String.class))
				return value;

			Method m = class2Value.get(type);
			if (m != null)
				return m.invoke(null, value);

			if (type.equals(java.lang.Character.TYPE) || type.equals(java.lang.Character.class))
				return value.charAt(0);

			Constructor<?> c = type.getConstructor(java.lang.String.class);
			return c.newInstance(value);
		} catch (NoSuchMethodException | IllegalAccessException | InstantiationException x) {
			LOG.debug("{}", x);
		} catch (InvocationTargetException x) {
			if (x.getTargetException() instanceof Error)
				throw (Error) x.getTargetException();
			LOG.debug("{}", x);
		}
		return null;
	}

	/* ------------------------------------------------------------ */
	/**
	 * Convert String value to instance.
	 * 
	 * @param type  classname or type (eg int)
	 * @param value The value as a string.
	 * @return The value as an Object.
	 */
	public static Object valueOf(String type, String value) {
		return valueOf(fromName(type), value);
	}

	/* ------------------------------------------------------------ */
	/**
	 * Parse an int from a substring. Negative numbers are not handled.
	 * 
	 * @param s      String
	 * @param offset Offset within string
	 * @param length Length of integer or -1 for remainder of string
	 * @param base   base of the integer
	 * @return the parsed integer
	 * @throws NumberFormatException if the string cannot be parsed
	 */
	public static int parseInt(String s, int offset, int length, int base) throws NumberFormatException {
		int value = 0;

		if (length < 0)
			length = s.length() - offset;

		for (int i = 0; i < length; i++) {
			char c = s.charAt(offset + i);

			int digit = convertHexDigit((int) c);
			if (digit < 0 || digit >= base)
				throw new NumberFormatException(s.substring(offset, offset + length));
			value = value * base + digit;
		}
		return value;
	}

	/* ------------------------------------------------------------ */
	/**
	 * Parse an int from a byte array of ascii characters. Negative numbers are not
	 * handled.
	 * 
	 * @param b      byte array
	 * @param offset Offset within string
	 * @param length Length of integer or -1 for remainder of string
	 * @param base   base of the integer
	 * @return the parsed integer
	 * @throws NumberFormatException if the array cannot be parsed into an integer
	 */
	public static int parseInt(byte[] b, int offset, int length, int base) throws NumberFormatException {
		int value = 0;

		if (length < 0)
			length = b.length - offset;

		for (int i = 0; i < length; i++) {
			char c = (char) (0xff & b[offset + i]);

			int digit = c - '0';
			if (digit < 0 || digit >= base || digit >= 10) {
				digit = 10 + c - 'A';
				if (digit < 10 || digit >= base)
					digit = 10 + c - 'a';
			}
			if (digit < 0 || digit >= base)
				throw new NumberFormatException(new String(b, offset, length));
			value = value * base + digit;
		}
		return value;
	}

	/* ------------------------------------------------------------ */
	public static byte[] parseBytes(String s, int base) {
		byte[] bytes = new byte[s.length() / 2];
		for (int i = 0; i < s.length(); i += 2)
			bytes[i / 2] = (byte) TypeUtil.parseInt(s, i, 2, base);
		return bytes;
	}

	/* ------------------------------------------------------------ */
	public static String toString(byte[] bytes, int base) {
		StringBuilder buf = new StringBuilder();
		for (byte b : bytes) {
			int bi = 0xff & b;
			int c = '0' + (bi / base) % base;
			if (c > '9')
				c = 'a' + (c - '0' - 10);
			buf.append((char) c);
			c = '0' + bi % base;
			if (c > '9')
				c = 'a' + (c - '0' - 10);
			buf.append((char) c);
		}
		return buf.toString();
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param c An ASCII encoded character 0-9 a-f A-F
	 * @return The byte value of the character 0-16.
	 */
	public static byte convertHexDigit(byte c) {
		byte b = (byte) ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10);
		if (b < 0 || b > 15)
			throw new NumberFormatException("!hex " + c);
		return b;
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param c An ASCII encoded character 0-9 a-f A-F
	 * @return The byte value of the character 0-16.
	 */
	public static int convertHexDigit(char c) {
		int d = ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10);
		if (d < 0 || d > 15)
			throw new NumberFormatException("!hex " + c);
		return d;
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param c An ASCII encoded character 0-9 a-f A-F
	 * @return The byte value of the character 0-16.
	 */
	public static int convertHexDigit(int c) {
		int d = ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10);
		if (d < 0 || d > 15)
			throw new NumberFormatException("!hex " + c);
		return d;
	}

	/* ------------------------------------------------------------ */
	public static void toHex(byte b, Appendable buf) {
		try {
			int d = 0xf & ((0xF0 & b) >> 4);
			buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));
			d = 0xf & b;
			buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	/* ------------------------------------------------------------ */
	public static void toHex(int value, Appendable buf) throws IOException {
		int d = 0xf & ((0xF0000000 & value) >> 28);
		buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));
		d = 0xf & ((0x0F000000 & value) >> 24);
		buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));
		d = 0xf & ((0x00F00000 & value) >> 20);
		buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));
		d = 0xf & ((0x000F0000 & value) >> 16);
		buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));
		d = 0xf & ((0x0000F000 & value) >> 12);
		buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));
		d = 0xf & ((0x00000F00 & value) >> 8);
		buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));
		d = 0xf & ((0x000000F0 & value) >> 4);
		buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));
		d = 0xf & value;
		buf.append((char) ((d > 9 ? ('A' - 10) : '0') + d));

		Integer.toString(0, 36);
	}

	/* ------------------------------------------------------------ */
	public static void toHex(long value, Appendable buf) throws IOException {
		toHex((int) (value >> 32), buf);
		toHex((int) value, buf);
	}

	/* ------------------------------------------------------------ */
	public static String toHexString(byte b) {
		return toHexString(new byte[] { b }, 0, 1);
	}

	/* ------------------------------------------------------------ */
	public static String toHexString(byte[] b) {
		return toHexString(b, 0, b.length);
	}

	/* ------------------------------------------------------------ */
	public static String toHexString(byte[] b, int offset, int length) {
		StringBuilder buf = new StringBuilder();
		for (int i = offset; i < offset + length; i++) {
			int bi = 0xff & b[i];
			int c = '0' + (bi / 16) % 16;
			if (c > '9')
				c = 'A' + (c - '0' - 10);
			buf.append((char) c);
			c = '0' + bi % 16;
			if (c > '9')
				c = 'a' + (c - '0' - 10);
			buf.append((char) c);
		}
		return buf.toString();
	}

	/* ------------------------------------------------------------ */
	public static byte[] fromHexString(String s) {
		if (s.length() % 2 != 0)
			throw new IllegalArgumentException(s);
		byte[] array = new byte[s.length() / 2];
		for (int i = 0; i < array.length; i++) {
			int b = Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16);
			array[i] = (byte) (0xff & b);
		}
		return array;
	}

	public static void dump(Class<?> c) {
		System.err.println("Dump: " + c);
		dump(c.getClassLoader());
	}

	public static void dump(ClassLoader cl) {
		System.err.println("Dump Loaders:");
		while (cl != null) {
			System.err.println("  loader " + cl);
			cl = cl.getParent();
		}
	}

	@Deprecated
	public static Object call(Class<?> oClass, String methodName, Object obj, Object[] arg)
			throws InvocationTargetException, NoSuchMethodException {
		throw new UnsupportedOperationException();
	}

	@Deprecated
	public static Object construct(Class<?> klass, Object[] arguments)
			throws InvocationTargetException, NoSuchMethodException {
		throw new UnsupportedOperationException();
	}

	@Deprecated
	public static Object construct(Class<?> klass, Object[] arguments, Map<String, Object> namedArgMap)
			throws InvocationTargetException, NoSuchMethodException {
		throw new UnsupportedOperationException();
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param o Object to test for true
	 * @return True if passed object is not null and is either a Boolean with value
	 *         true or evaluates to a string that evaluates to true.
	 */
	public static boolean isTrue(Object o) {
		if (o == null)
			return false;
		if (o instanceof Boolean)
			return ((Boolean) o).booleanValue();
		return Boolean.parseBoolean(o.toString());
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param o Object to test for false
	 * @return True if passed object is not null and is either a Boolean with value
	 *         false or evaluates to a string that evaluates to false.
	 */
	public static boolean isFalse(Object o) {
		if (o == null)
			return false;
		if (o instanceof Boolean)
			return !((Boolean) o).booleanValue();
		return "false".equalsIgnoreCase(o.toString());
	}

	/* ------------------------------------------------------------ */
	public static URI getLocationOfClass(Class<?> clazz) {
		try {
			ProtectionDomain domain = clazz.getProtectionDomain();
			if (domain != null) {
				CodeSource source = domain.getCodeSource();
				if (source != null) {
					URL location = source.getLocation();

					if (location != null)
						return location.toURI();
				}
			}

			String resourceName = clazz.getName().replace('.', '/') + ".class";
			ClassLoader loader = clazz.getClassLoader();
			URL url = (loader == null ? ClassLoader.getSystemClassLoader() : loader).getResource(resourceName);
			if (url != null) {
				return URIUtil.getJarSource(url.toURI());
			}
		} catch (URISyntaxException e) {
			LOG.debug("{}", e);
		}
		return null;
	}

	public static Long castToLong(Object value) {
		if (value == null) {
			return null;
		}

		if (value instanceof Number) {
			return ((Number) value).longValue();
		}

		if (value instanceof String) {
			String strVal = (String) value;
			if (strVal.length() == 0 //
					|| "null".equals(strVal) //
					|| "NULL".equals(strVal)) {
				return null;
			}

			if (strVal.indexOf(',') != 0) {
				strVal = strVal.replaceAll(",", "");
			}

			try {
				return Long.parseLong(strVal);
			} catch (NumberFormatException ex) {
				//
			}
		}

		if (value instanceof Map) {
			Map map = (Map) value;
			if (map.size() == 2 && map.containsKey("andIncrement") && map.containsKey("andDecrement")) {
				Iterator iter = map.values().iterator();
				iter.next();
				Object value2 = iter.next();
				return castToLong(value2);
			}
		}

		return null;
	}

	public static String castToString(Object value) {
		if (value == null) {
			return null;
		}

		return value.toString();
	}
}
